local _G = _G

local date = _G.date
local pairs = _G.pairs
local select = _G.select
local time = _G.time

local GetItemInfo = _G.GetItemInfo
local GetNumRaidMembers = _G.GetNumRaidMembers
local GetNumSavedInstances = _G.GetNumSavedInstances
local GetRealZoneText = _G.GetRealZoneText
local GetSavedInstanceInfo = _G.GetSavedInstanceInfo
local GetTime = _G.GetTime
local IsInInstance = _G.IsInInstance
local UnitAffectingCombat = _G.UnitAffectingCombat
local UnitClass = _G.UnitClass
local UnitExists = _G.UnitExists
local UnitIsAFK = _G.UnitIsAFK
local UnitIsConnected = _G.UnitIsConnected
local UnitIsDeadOrGhost = _G.UnitIsDeadOrGhost
local UnitIsFeignDeath = _G.UnitIsFeignDeath
local UnitIsVisible = _G.UnitIsVisible
local UnitName = _G.UnitName

--[[ these pattern might get changed later
local UNITDIESOTHER = _G.UNITDIESOTHER
local LOOT_ITEM = _G.LOOT_ITEM
local LOOT_ITEM_MULTIPLE = _G.LOOT_ITEM_MULTIPLE
local LOOT_ITEM_SELF = _G.LOOT_ITEM_SELF
local LOOT_ITEM_SELF_MULTIPLE = _G.LOOT_ITEM_SELF_MULTIPLE
-- ]]

local AceLibrary = _G.AceLibrary

local Secretary = AceLibrary("AceAddon-2.0"):new("AceConsole-2.0", "AceEvent-2.0")

local Deformat = AceLibrary("Deformat-2.0")
local BB = AceLibrary("Babble-Boss-2.2")
local BS = AceLibrary("Babble-Spell-2.2")
local Roster = AceLibrary("Roster-2.1")
local SEA = AceLibrary("SpecialEvents-Aura-2.0")

function Secretary:NewDB()
	local db = {
		raids = {},
		chars = {},
		events = {},
	}
	db.config = _G.SecretaryDB and _G.SecretaryDB.config or {
		quality = 3,
		check_period = 1,
		silent = false,
	}
	_G.SecretaryDB = db
	self.db = db
end

function Secretary:Reset()
	self:StopRaid()
	self:NewDB()
end

function Secretary:OnInitialize()
	local db = _G.SecretaryDB
	if not db then
		self:NewDB()
	else
		self.db = db
	end
	self.attendance = {}
	self.removed = {}
	self.Output = db.silent and function () end or self.Print
end

function Secretary:OnEnable()
	if self.db.recording then
		self:Output("Restarting the records for raid #"..self.db.raid_id)
		self:StartEventCapture()
	end
	self:RegisterEvent("RosterLib_RosterUpdated")
	self:RosterLib_RosterUpdated()
end

function Secretary:RosterLib_RosterUpdated()
	if GetNumRaidMembers() > 0 then
		if self.db.recording then
			self:UpdateRaidAttendanceCache()
		else
			self:NewRaid()
		end
	end
end

--[[ Characters API ]]

function Secretary:NewCharacter(name, class, comment)
	local chars = self.db.chars
	local entry = chars[name]
	if entry then
		if entry.class ~= class then
			self:Output("Warning: changed class from '%s' to '%s' for char '%s'", entry.class, class, name)
		end
		
	else
		entry = {}
		chars[name] = entry
	end
	entry.class = class
	entry.comment = comment
end

function Secretary:CheckCharacterForUnitInDB(unit)
	local name = UnitName(unit)
	if name and not self.db.chars[name] then
		self:NewCharacter(name, select(2, UnitClass(unit)), ("Automatically added on %s"):format(date()))
	end
end

function Secretary:GetCharacterInfo(name)
	local entry = self.db.chars[name]
	if entry then
		return entry.class, entry.comment
	end
end

function Secretary:GetCharacterClass(name)
	local entry = self.db.chars[name]
	if entry then
		return entry.class
	end
end

function Secretary:GetCharacterComment(name)
	local entry = self.db.chars[name]
	if entry then
		return entry.comment
	end
end

function Secretary:SetCharacterComment(name, newcomment)
	local entry = self.db.chars[name]
	if entry then
		entry.comment = newcomment
	end
end

--[[ Raid API ]]

function Secretary:NewRaid(comment)
	local raids = self.db.raids
	local id = #raids + 1
	raids[id] = {
		instances = {},
		zones = {},
		comment = comment,
		start_date = date(),
		start_time = GetTime(),
	}
	self.db.raid_id = id
	self:Output("Information: Recording raid events for new raid %d", #raids)
	self.attendance = {}
	self:UpdateRaidAttendanceCache()
	self:StartEventCapture()
end

function Secretary:StopRaid()
	local db = self.db
	local raid_id = db.raid_id
	if raid_id and db.recording then
		local raids = db.raids
		local raid = raids[raid_id]
		raid.end_date = date()
		raid.end_time = GetTime()
		
		self:StopEventCapture()
		db.recording = nil
		db.raid_id = nil
		self:Output("Information: stopped raid %d", raid_id)
	end
end

function Secretary:GetRaidInfo(index)
	local entry = self.db.raids[index]
	if entry then
		return entry.type, entry.zone, entry.start, entry.comment
	end
end

function Secretary:GetRaidType(index)
	local entry = self.db.raids[index]
	if entry then
		return entry.type
	end
end

function Secretary:GetRaidZone(index)
	local entry = self.db.raids[index]
	if entry then
		return entry.zone
	end
end

function Secretary:GetRaidStart(index)
	local entry = self.db.raids[index]
	if entry then
		return entry.start
	end
end

function Secretary:GetRaidComment(index)
	local entry = self.db.raids[index]
	if entry then
		return entry.comment
	end
end

function Secretary:SetRaidComment(index, newcomment)
	local entry = self.db.raids[index]
	if entry then
		entry.comment = newcomment
	end
end

--[[ Events ]]

function Secretary:StartEventCapture()
	if self.capturing then return end
	self:Output("Information: Started recording raid events")
--	self:RegisterEvent("UPDATE_INSTANCE_INFO")
	self:RegisterEvent("CHAT_MSG_COMBAT_HOSTILE_DEATH")
	self:RegisterEvent("CHAT_MSG_LOOT")
	self:RegisterEvent("CHAT_MSG_SYSTEM")
	self:RegisterEvent("PLAYER_ENTERING_WORLD")
	self.capturing = true
	self:ScheduleRepeatingEvent("Secretary:CheckRaidStatus", self.CheckRaidStatus, self.db.config.check_period, self)
end

function Secretary:StopEventCapture()
	if not self.capturing then return end
	self:Output("Information: Stopped recording raid events")
--[[
	if self:IsEventRegistered("UPDATE_INSTANCE_INFO") then
		self:UnregisterEvent("UPDATE_INSTANCE_INFO")
	end
]]
	self:UnregisterEvent("CHAT_MSG_COMBAT_HOSTILE_DEATH")
	self:UnregisterEvent("CHAT_MSG_LOOT")
	self:RegisterEvent("CHAT_MSG_SYSTEM")
	self:RegisterEvent("PLAYER_ENTERING_WORLD")
	self:CancelScheduledEvent("Secretary:CheckRaidStatus")
	self.capturing = nil
end

local function UnitIsOutOfCombat(unit)
--[[
	Out of Combat units are units that have stopped
	being part of the current combat. Either because they died,
	or have used a particular ability that gets them out of the
	threat list.
]]
	if 
		UnitIsDeadOrGhost(unit) or 
		UnitIsFeignDeath(unit) or
		SEA:UnitHasBuff(unit, BS["Divine Intervention"]) or
		SEA:UnitHasBuff(unit, BS["Stealth"]) or -- vanished rogues don't count
		SEA:UnitHasBuff(unit, BS["Invisibility"]) or -- invisible mages don't count either
		-- this one is difficult, but we want to check that player that may have released and rezzed are indeed ooc
		not UnitIsVisible(unit) -- fixme: check first if the leader has released (i.e. GetRealZoneText() == raid.zone)
	then
		return true
	end
end

function Secretary:UpdateRaidAttendanceCache()
	local db = self.db
	
	local removed = self.removed
	local attendance = self.attendance
	
	local raid_id = db.raid_id
	local date = date()
	local time = GetTime()
	local events = db.events
	for name, state in pairs(attendance) do
		removed[name] = true
	end
	local unit_in_combat, not_out_of_combat = 0, 0
	for i = 1, 40 do
		local unit = "raid"..i
		
		if UnitExists(unit) then
			self:CheckCharacterForUnitInDB(unit)
			
			local name = UnitName(unit)
			if name and name ~= _G.UNKNOWN then
				local connected = UnitIsConnected(unit)
				local afk = UnitIsAFK(unit)
				local state = attendance[name]
				
				removed[name] = nil
				
				if not state then
					state = "active"
					events[#events + 1] = { 
						type = "enter",
						raid_id = raid_id,
						player = name,
						time = time
					}
					attendance[name] = "active"
				end
				
				if not connected then
					if state ~= "offline" then
						events[#events + 1] = {
							type = "offline",
							raid_id = raid_id,
							player = name, 
							time = time
						}
						attendance[name] = "offline"
					end
				elseif afk then
					if state ~= "afk" then
						events[#events + 1] = {
							type = "afk",
							raid_id = raid_id,
							player = name,
							time = time,
						}
						attendance[name] = "afk"
					end
				elseif state ~= "active" then
					events[#events + 1] = {
						type = "return",
						raid_id = raid_id,
						player = name,
						time = time,
					}
					attendance[name] = "active"
				end
				if UnitAffectingCombat(unit) then
					unit_in_combat = unit_in_combat + 1
				end
				if connected and not UnitIsOutOfCombat(unit) then
					not_out_of_combat = not_out_of_combat + 1
				end
			end
		end
	end
	for name in pairs(removed) do
		events[#events + 1] = {
			type = "leave",
			raid_id = raid_id,
			player = name, 
			time = time
		}
		removed[name] = nil
		attendance[name] = nil
	end
	self:CheckForWipe(unit_in_combat, not_out_of_combat)
end

function Secretary:CheckForWipe(unit_in_combat, not_out_of_combat)
	if not self.fighting then
		if unit_in_combat > 0 then
			self.fighting = true
		end
	elseif unit_in_combat == 0 then
		if not_out_of_combat == 0 then
			local db = self.db
			local events = db.events
			events[#events + 1] = {
				type = "wipe",
				raid_id = db.raid_id,
				time = GetTime()
			}
			self:Output("Information: Recorded a wipe !")
		end
		self.fighting = nil
	end
end

function Secretary:CheckRaidStatus()
	self:UpdateRaidAttendanceCache()
	if GetNumRaidMembers() == 0 then
		self:StopRaid()
	else
		self.db.recording = true
	end
end

--[[
function Secretary:UPDATE_INSTANCE_INFO()
	local db = self.db
	local raid_id = db.raid_id
	local raids = db.raids
	local raid = raids[raid_id]
	
	local raid_zone = raid.zone

	if not raid_zone or raid.blizzard_id then return end
	
	for i = 1, GetNumSavedInstances() do
		local zone, id, reset = GetSavedInstanceInfo(i)
		if raid_zone == zone then
			raid.blizzard_id = id
			self:UnregisterEvent("UPDATE_INSTANCE_INFO")
			return
		end
	end
	self:Output("Warning: Unable to find the current raid id. (%s)", raid_zone)
end
]]

function Secretary:CHAT_MSG_COMBAT_HOSTILE_DEATH(msg)
	local name = Deformat(msg, _G.UNITDIESOTHER)
	if name and BB:HasReverseTranslation(name) then
	
		local db = self.db
		local events = db.events
		events[#events + 1] = {
			type = "kill",
			raid_id = db.raid_id,
			name = name,
			time = GetTime(),
		}
		self:Output("Information: Recorded kill of %s", name)
	end
end

function Secretary:CHAT_MSG_LOOT(msg)
	local name, itemlink, count
	
	name, itemlink, count = Deformat(msg, _G.LOOT_ITEM_MULTIPLE)
	if not itemlink then
		name, itemlink = Deformat(msg, _G.LOOT_ITEM)
		count = 1
	end
	if not itemlink then
		name = UnitName("player")
		itemlink, count = Deformat(msg, _G.LOOT_ITEM_SELF_MULTIPLE)
		if not itemlink then
			itemlink = Deformat(msg, _G.LOOT_ITEM_SELF)
			count = 1
		end
	end
	
	if itemlink then
		local quality = select(3, GetItemInfo(itemlink))
		local db = self.db

		if quality < db.config.quality then return end

		local events = db.events
		events[#events + 1] = {
			type = "loot",
			raid_id = db.raid_id,
			name = name,
			item = itemlink,
			count = count,
			time = GetTime(),
		}
		self:Output("Information: Recorded that %s looted %s %s", name, count, itemlink)
	end
end

function Secretary:CHAT_MSG_SYSTEM(msg)
	local instance, remaining = Deformat(msg, _G.RAID_INSTANCE_WELCOME)
	if instance then
		local db = self.db
		local instances = db.raids[db.raid_id].instances
		
		if not instances[instance] then
			local instance_id = false
			for i = 1, GetNumSavedInstances() do
				local name, id = GetSavedInstanceInfo(i)
				if name == instance then
					self:Output("Information: Current raid associated with %s (%s)", name, id)
					instance_id = id
					break
				end
			end
			instances[instance] = instance_id
			if not instance_id then
				self:Output("Information: Current raid associated with %s (no instance_id found yet)", instance)
			end
		end
	end
end

function Secretary:PLAYER_ENTERING_WORLD()
	if IsInInstance() then
		-- we wait a little bit to check for the zone name
		self:ScheduleEvent("Secretary:CheckInstanceZone", self.CheckInstanceZone, 0.5, self)
	end
end

function Secretary:CheckInstanceZone()
	local result, type = IsInInstance()
	if result then
		local db = self.db
		if not db.raid_id then return end
		local zones = db.raids[db.raid_id].zones
		local zone = GetRealZoneText()
		if not zones[zone] then
			zones[zone] = true
			self:Output("Information: Current raid associated with zone %s", zone)
		end
	end
end

_G.Secretary = Secretary